Skip to content

feat(kanban): card attachments#40

Merged
cnjack merged 3 commits into
mainfrom
feat/kanban-b3-attachments
Jun 23, 2026
Merged

feat(kanban): card attachments#40
cnjack merged 3 commits into
mainfrom
feat/kanban-b3-attachments

Conversation

@cnjack

@cnjack cnjack commented Jun 22, 2026

Copy link
Copy Markdown
Owner

What

Implements B3 — an Attachments section on the card peek: a list of attachment links (URLs or vault paths) with add / remove, plus file upload where the platform provides an uploader.

Following the data-location rule: file-board values live in frontmatter attachments (so they ride document sync to desktop + web); DB-board values live in properties_extra.

Changes

  • shared/lib/board.tscard.attachments + parseAttachments / serializeAttachments / attachmentName.
  • jtype-corescan_board_cards parses attachments frontmatter into BoardCardInfo (parse_attachments — comma-split, kept verbatim, no #/[] stripping).
  • BoardPeek — Attachments list (clickable, basename label, remove ✕) + a "paste a URL or path" input + an Upload button when onUploadAttachment is supplied.
  • BoardSurface / types — thread onUploadAttachment through to the peek.
  • Adapters: desktop + web file boards read/write the attachments frontmatter; the DB board reads/writes properties_extra. Web boards wire upload to api.uploadAsset; desktop uses URL/path entry.
  • tests/unit/boardAttachments.spec.ts + i18n (zh).

Verification

  • cargo check + 34 jtype-core tests pass; root + web tsc clean; unit tests pass.
  • Throwaway harness confirmed the attachment list (correct basenames spec.pdf / home.png, clickable links), add-via-input (added notes.md), remove (✕ removed spec.pdf), and the upload control.

Follow-up

Desktop file upload via the blob channel; inline image previews.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Board cards now support attachments—users can upload files or paste URLs/paths
    • Attachment list displays in card preview with safe URL validation
    • Unsafe links are blocked and users receive a warning
    • Added multi-language support for attachment UI (English, Japanese, Korean, Chinese)
  • Tests

    • Added comprehensive unit tests for attachment parsing, serialization, and URL safety validation

Add an Attachments section to the card peek: a list of attachment links (URLs or
vault paths) with add / remove, plus a file upload where the platform provides an
uploader. File-board values live in frontmatter `attachments` (so they ride
document sync to desktop + web); DB-board values live in properties_extra.

- shared/lib/board.ts: card.attachments + parseAttachments / serializeAttachments
  / attachmentName helpers.
- jtype-core: scan_board_cards parses `attachments` frontmatter into BoardCardInfo
  (parse_attachments — comma-split, kept verbatim, no #/[] stripping).
- BoardPeek: Attachments list (clickable, basename label, remove ✕) + "paste URL
  or path" input + an Upload button when onUploadAttachment is supplied.
- BoardSurface/types: thread onUploadAttachment through to the peek.
- Adapters: desktop + web file boards read/write the `attachments` frontmatter;
  the DB board reads/writes properties_extra. Web boards wire upload to
  api.uploadAsset; desktop uses URL/path entry.
- tests/unit/boardAttachments.spec.ts + i18n (zh).

Verified: cargo check + 34 jtype-core tests, root+web tsc, unit tests, and a
throwaway harness confirmed the attachment list (correct basenames + links),
add-via-input, remove, and the upload control.

Follow-up: desktop file upload via the blob channel; inline image previews.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds attachment support to board cards end-to-end. Shared utilities (parseAttachments, serializeAttachments, attachmentName, isSafeAttachmentUrl) and type extensions are introduced. The Rust BoardCardInfo struct gains an attachments field. BoardPeek receives a full attachment management UI with safe-link validation, URL paste, and optional file upload. Three platform consumers (desktop BoardView, Kanban, WebBoardView) wire the data flow and upload callback. i18n catalogs for en/ja/ko/zh are updated.

Changes

Card Attachments for Board

Layer / File(s) Summary
Shared attachment data model and utilities
shared/lib/board.ts, src/lib/types.ts, services/jtype-core/src/lib.rs, tests/unit/boardAttachments.spec.ts
BoardViewCard and BoardCard gain optional attachments?: string[]. Four helpers added to shared/lib/board.ts: parseAttachments (comma-split/trim), serializeAttachments (join), attachmentName (decoded basename), isSafeAttachmentUrl (scheme allowlist). Rust BoardCardInfo gains attachments: Vec<String> field and parse_attachments helper, populated in scan_board_cards_inner. Unit tests cover all four helpers.
BoardPeek and BoardSurface attachment UI
shared/components/board/types.ts, shared/components/board/BoardSurface.tsx, shared/components/board/BoardPeek.tsx
BoardSurfaceProps gains optional onUploadAttachment?: (file: File) => Promise<string>. BoardSurface destructures and forwards it to BoardPeek. BoardPeek gains newAttach/uploading state, dedup-add and handleUpload logic, and a new attachments section rendering safe/unsafe links, per-item removal, URL paste form, and a conditional file upload control.
Platform consumers: desktop BoardView, Kanban, WebBoardView
src/components/BoardView.tsx, services/jtype-web/frontend/src/pages/Kanban.tsx, services/jtype-web/frontend/src/pages/WebBoardView.tsx
Desktop BoardView maps c.attachments onto cards and serializes patch.attachments on update. Kanban normalizes propertiesExtra.attachments, patches or deletes the key on update, and provides onUploadAttachment via api.uploadAsset. WebBoardView parses attachments on load, serializes on update, and wires onUploadAttachment returning a.url.
i18n strings
shared/i18n/locales/en/..., shared/i18n/locales/ja/..., shared/i18n/locales/ko/..., shared/i18n/locales/zh/...
Adds "Attachments", "Paste a URL or path", "Remove", "unsafe", "Unsafe link blocked: {url}", "Upload", and "Uploading…" to en/ja/ko/zh .po catalogs and regenerates the compiled .mjs message maps.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A bunny found a card one day,
with papers clipped and tucked away.
She parsed each path with comma care,
blocked sketchy links beyond repair.
Now files upload with hoppy glee —
attachments safe as safe can be! 📎

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(kanban): card attachments' clearly and concisely summarizes the main feature addition—card attachments functionality for the Kanban board—which aligns with the extensive changeset implementing this feature across multiple files and components.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/kanban-b3-attachments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

cnjack and others added 2 commits June 23, 2026 11:57
Attachment values are user-supplied, so a `javascript:`/`data:`/`vbscript:`/`file:`
"URL" rendered into `<a href>` would execute on click. Add isSafeAttachmentUrl
(allows http(s) + scheme-less relative paths, blocks everything else) and render
unsafe attachments as inert text marked "(unsafe)" instead of a link.

- shared/lib/board.ts: isSafeAttachmentUrl helper.
- BoardPeek: conditional <a> vs inert <span> for the attachment.
- tests/unit/boardAttachments.spec.ts: cover safe/dangerous schemes.
- i18n: new strings + zh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ments

# Conflicts:
#	services/jtype-core/src/lib.rs
#	services/jtype-web/frontend/src/pages/Kanban.tsx
#	services/jtype-web/frontend/src/pages/WebBoardView.tsx
#	shared/components/board/BoardPeek.tsx
#	shared/components/board/BoardSurface.tsx
#	shared/components/board/types.ts
#	shared/i18n/locales/en/messages.mjs
#	shared/i18n/locales/en/messages.po
#	shared/i18n/locales/ja/messages.mjs
#	shared/i18n/locales/ja/messages.po
#	shared/i18n/locales/ko/messages.mjs
#	shared/i18n/locales/ko/messages.po
#	shared/i18n/locales/zh/messages.mjs
#	shared/i18n/locales/zh/messages.po
#	shared/lib/board.ts
#	src/components/BoardView.tsx
#	src/lib/types.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/jtype-web/frontend/src/pages/WebBoardView.tsx`:
- Line 232: In the WebBoardView.tsx file where attachments are serialized, the
current code assigns serialized attachments to next.attachments whenever
patch.attachments is defined, but this creates an empty string when the
attachments list is cleared. Instead of always serializing when defined, add an
additional check to verify the attachments array has items (length > 0) before
calling serializeAttachments and assigning to next.attachments. If the
attachments list is empty, either omit the assignment entirely or explicitly
delete the next.attachments key to ensure consistent handling of cleared
attachments across consumers.

In `@shared/components/board/BoardPeek.tsx`:
- Around line 386-394: The isSafeAttachmentUrl() function currently allows
scheme-less URLs (starting with //) to be treated as safe, but browsers
interpret these as protocol-relative URLs that resolve to another origin,
creating a security bypass. Update the isSafeAttachmentUrl() function to
explicitly reject URLs that start with // before checking other validation
logic, ensuring these scheme-less attachment URLs are properly blocked and
displayed as unsafe.
- Around line 418-429: The onChange handler in the file input element is calling
handleUpload() with a void operator, which suppresses the returned promise and
causes unhandled rejections if the upload fails. Remove the void operator and
add proper error handling to catch any failures from handleUpload(). This can be
done by either chaining a .catch() method to handle errors, or wrapping the call
in proper async/await with try/catch. Ensure that upload failures are properly
communicated to the user instead of silently failing.

In `@shared/i18n/locales/ja/messages.po`:
- Around line 73-76: Several attachment-related strings have empty Japanese
translations (msgstr) in the ja/messages.po file at the specified line ranges.
Fill in the Japanese translations for all seven msgid entries: "Attachments",
"Paste a URL or path", "Remove", "unsafe", "Unsafe link blocked: {url}",
"Upload", and "Uploading…". Each empty msgstr needs to be populated with the
appropriate Japanese translation to ensure the UI displays correctly in Japanese
instead of falling back to English.

In `@shared/i18n/locales/ko/messages.mjs`:
- Line 1: Translate the new attachment-related message strings from English to
Korean in the messages.mjs file. Locate the following message keys in the JSON:
ONWvwQ (Upload), Pvpx7b (Paste a URL or path), t_YqKh (Remove), w7E-FA (Unsafe
link blocked with url parameter), and w_Sphq (Attachments), and replace their
English values with appropriate Korean translations to ensure the Korean locale
is fully localized.

In `@shared/lib/board.ts`:
- Around line 72-83: The parseAttachments and serializeAttachments functions use
commas as delimiters, but valid vault paths and URLs can contain commas, causing
data corruption when attachments with commas are persisted and parsed back.
Replace the comma delimiter with a safer alternative (such as newline or pipe
character) in both functions: update the split(",") call in parseAttachments and
the join(", ") call in serializeAttachments to use the new delimiter
consistently. Ensure the same delimiter change is applied to the corresponding
code in services/jtype-core/src/lib.rs at Line 1367 to maintain consistency
across both platforms.

In `@src/components/BoardView.tsx`:
- Line 181: The line where patch.attachments is processed needs to handle empty
arrays specially. When patch.attachments is an empty array, instead of calling
serializeAttachments which converts it to an empty string (causing round-trip
issues), delete the attachments key from the next object. Only call
serializeAttachments and assign to next.attachments when the attachments array
has actual content. Modify the assignment at line 181 to check if
patch.attachments is non-empty before serializing, and delete next.attachments
when the array is empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 48f5ed05-56bd-49e8-be00-3b0182f4debc

📥 Commits

Reviewing files that changed from the base of the PR and between 899ea4a and dcf475c.

📒 Files selected for processing (18)
  • services/jtype-core/src/lib.rs
  • services/jtype-web/frontend/src/pages/Kanban.tsx
  • services/jtype-web/frontend/src/pages/WebBoardView.tsx
  • shared/components/board/BoardPeek.tsx
  • shared/components/board/BoardSurface.tsx
  • shared/components/board/types.ts
  • shared/i18n/locales/en/messages.mjs
  • shared/i18n/locales/en/messages.po
  • shared/i18n/locales/ja/messages.mjs
  • shared/i18n/locales/ja/messages.po
  • shared/i18n/locales/ko/messages.mjs
  • shared/i18n/locales/ko/messages.po
  • shared/i18n/locales/zh/messages.mjs
  • shared/i18n/locales/zh/messages.po
  • shared/lib/board.ts
  • src/components/BoardView.tsx
  • src/lib/types.ts
  • tests/unit/boardAttachments.spec.ts

if (patch.due !== undefined) next.due = patch.due ?? ''
if (patch.icon !== undefined) next.icon = patch.icon ?? ''
if (patch.tags !== undefined) next.tags = patch.tags.map((t) => t.label).join(', ')
if (patch.attachments !== undefined) next.attachments = serializeAttachments(patch.attachments)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Delete attachments when empty instead of serializing to "" (Line 232).

Writing an empty string for cleared attachments can produce inconsistent round-trips across consumers. Remove the key when list length is zero.

Suggested fix
-        if (patch.attachments !== undefined) next.attachments = serializeAttachments(patch.attachments)
+        if (patch.attachments !== undefined) {
+          if (patch.attachments.length > 0) next.attachments = serializeAttachments(patch.attachments)
+          else delete next.attachments
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (patch.attachments !== undefined) next.attachments = serializeAttachments(patch.attachments)
if (patch.attachments !== undefined) {
if (patch.attachments.length > 0) next.attachments = serializeAttachments(patch.attachments)
else delete next.attachments
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/jtype-web/frontend/src/pages/WebBoardView.tsx` at line 232, In the
WebBoardView.tsx file where attachments are serialized, the current code assigns
serialized attachments to next.attachments whenever patch.attachments is
defined, but this creates an empty string when the attachments list is cleared.
Instead of always serializing when defined, add an additional check to verify
the attachments array has items (length > 0) before calling serializeAttachments
and assigning to next.attachments. If the attachments list is empty, either omit
the assignment entirely or explicitly delete the next.attachments key to ensure
consistent handling of cleared attachments across consumers.

Comment on lines +386 to +394
{isSafeAttachmentUrl(url) ? (
<a href={url} target="_blank" rel="noreferrer" className="flex-1 truncate text-brand-dark hover:underline" title={url}>
{attachmentName(url)}
</a>
) : (
<span className="flex-1 truncate text-stone-500" title={t`Unsafe link blocked: ${url}`}>
{attachmentName(url)} <span className="text-red-500">({t`unsafe`})</span>
</span>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject //host attachment URLs.

isSafeAttachmentUrl() still treats scheme-less //... values as safe, but browsers resolve them to another origin. That lets a pasted attachment bypass the unsafe-link block and stay clickable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/components/board/BoardPeek.tsx` around lines 386 - 394, The
isSafeAttachmentUrl() function currently allows scheme-less URLs (starting with
//) to be treated as safe, but browsers interpret these as protocol-relative
URLs that resolve to another origin, creating a security bypass. Update the
isSafeAttachmentUrl() function to explicitly reject URLs that start with //
before checking other validation logic, ensuring these scheme-less attachment
URLs are properly blocked and displayed as unsafe.

Comment on lines +418 to +429
{onUploadAttachment && (
<label className="inline-flex shrink-0 cursor-pointer items-center gap-1 rounded-md border border-stone-200 px-2 py-1 text-xs text-stone-600 hover:border-brand/40 hover:text-brand-dark">
<ArrowUpTrayIcon className="h-3.5 w-3.5" />
{uploading ? <Trans>Uploading…</Trans> : <Trans>Upload</Trans>}
<input
type="file"
className="hidden"
disabled={uploading}
onChange={(e) => {
void handleUpload(e.target.files?.[0]);
e.target.value = "";
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle upload failures before discarding the promise.

handleUpload() can reject here, but the void call drops the promise. That turns an upload failure into an unhandled rejection and gives the user no feedback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/components/board/BoardPeek.tsx` around lines 418 - 429, The onChange
handler in the file input element is calling handleUpload() with a void
operator, which suppresses the returned promise and causes unhandled rejections
if the upload fails. Remove the void operator and add proper error handling to
catch any failures from handleUpload(). This can be done by either chaining a
.catch() method to handle errors, or wrapping the call in proper async/await
with try/catch. Ensure that upload failures are properly communicated to the
user instead of silently failing.

Comment on lines +73 to +76
#: shared/components/board/BoardPeek.tsx
msgid "Attachments"
msgstr ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fill Japanese translations for new attachment strings.

These newly added entries are shipped with empty msgstr, so the Japanese UI will show English fallback for the attachment flow (Attachments, Paste a URL or path, Remove, unsafe, Unsafe link blocked: {url}, Upload, Uploading…).

Also applies to: 339-342, 370-373, 482-489, 502-509

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/i18n/locales/ja/messages.po` around lines 73 - 76, Several
attachment-related strings have empty Japanese translations (msgstr) in the
ja/messages.po file at the specified line ranges. Fill in the Japanese
translations for all seven msgid entries: "Attachments", "Paste a URL or path",
"Remove", "unsafe", "Unsafe link blocked: {url}", "Upload", and "Uploading…".
Each empty msgstr needs to be populated with the appropriate Japanese
translation to ensure the UI displays correctly in Japanese instead of falling
back to English.

@@ -1 +1 @@
/*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"Blocked by\"],\"-b7T3G\":[\"Updated\"],\"1DBGsz\":[\"노트\"],\"1YABGm\":[\"링크 (Ctrl+K)\"],\"1hKEom\":[\"우선순위\"],\"2wxgft\":[\"이름 변경\"],\"3qkggm\":[\"전체 화면\"],\"4gdyen\":[\"로컈 (내 것)\"],\"4hJhzz\":[\"테이블\"],\"54sFiP\":[\"flowchart TD\\n A[시작] --> B[끝]\"],\"5Q_DQ6\":[\"인라인 코드\"],\"7VpPHA\":[\"확인\"],\"7s3WlU\":[\"Blocks\"],\"8PifYj\":[\"Mermaid 다이어그램\"],\"8hSn0h\":[\"결과 (편집 가능)\"],\"8lE269\":[\"정렬: 수동\"],\"9gxam6\":[\"이 Draw.io 다이어그램을 렌더링할 수 없습니다.\"],\"AC9Gkf\":[\"열 펼치기\"],\"AS5WO9\":[\"이 PDF를 렌더링할 수 없습니다.\"],\"AVreQ5\":[\"드래그하여 크기 조정\"],\"AgvHni\":[\"열 추가\"],\"AxAubu\":[\"그룹: 담당자\"],\"BfMZ7w\":[\"클라우드 수낙\"],\"BnmEvM\":[\"템플릿으로 저장\"],\"C6-ZRl\":[\"Someone\"],\"EWPtMO\":[\"코드\"],\"EbMPZJ\":[\"미할당\"],\"G4qrLy\":[\"완료 열 해제\"],\"GKu3m4\":[\"라벨 없음\"],\"Gpfctt\":[\"마감\"],\"H_SQFv\":[\"색상 없음\"],\"I6SWEy\":[\"스플릿\"],\"ICip_B\":[\"클라우드 (원격)\"],\"Ik60OC\":[\"에디터에서 열기\"],\"Iw6WJa\":[\"WIP 한도 설정\"],\"JTYvAw\":[\"카드 검색\"],\"K_F6pa\":[\"저장 중…\"],\"KjXDqG\":[\"Swimlane: None\"],\"KmydK6\":[\"굵게\"],\"KvW1VO\":[\"Draw.io 다이어그램\"],\"LQn6-8\":[\"로컈 수낙\"],\"MHrjPM\":[\"제목\"],\"Mm72la\":[\"No comments yet\"],\"NBdIgR\":[\"Comment\"],\"OYHzN1\":[\"태그\"],\"OepdfE\":[\"그룹: 상태\"],\"Q2mGA7\":[\"필터 지우기\"],\"QD8opX\":[\"보드\"],\"QlsPZy\":[\"Mermaid 구문을 작성하면 다이어그램이 표시됩니다.\"],\"S5Qbb1\":[\"쉼표로 구분\"],\"TdfEV7\":[\"Archived\"],\"UQOvxZ\":[\"빈 카드\"],\"VNa_N2\":[\"이 파일 형식은 아직 미리볼 수 없습니다.\"],\"VbyRUy\":[\"Comments\"],\"WSP6v1\":[\"정렬: 우선순위\"],\"X03-eC\":[\"값을 입력해 주세요.\"],\"XJOV1Y\":[\"Activity\"],\"Ya7bZl\":[\"다이어그램 오류\"],\"Zot9XS\":[\"카드 없음\"],\"_5CsXX\":[\"완료 열\"],\"_EsjyQ\":[\"이것 사용\"],\"a6uhHr\":[\"굵게 (Ctrl+B)\"],\"aDvLhk\":[\"Add a comment…\"],\"abUZlY\":[\"세부정보 추가...\"],\"agOeRN\":[\"이 API 명세를 렌더링할 수 없습니다.\"],\"b4hVKD\":[\"색상 열\"],\"cfaWH-\":[\"라벨 추가\"],\"cnGeoo\":[\"삭제\"],\"d-F6q9\":[\"Created\"],\"d5z6xQ\":[\"WIP 한도 \",[\"0\"]],\"dEgA5A\":[\"취소\"],\"euc6Ns\":[\"복제\"],\"fYcKtB\":[\"정렬: 마감\"],\"gLDJuJ\":[\"제목 없는 카드\"],\"hh4sEG\":[\"Relates\"],\"hnK1gR\":[\"PDF 문서\"],\"i4_LY_\":[\"작성\"],\"iTylMl\":[\"템플릿\"],\"iYVqZq\":[\"열 이름\"],\"jUbC3Z\":[\"Swimlane: Priority\"],\"jZlrte\":[\"색상\"],\"kZlRKE\":[\"Mermaid 소스\"],\"kryGs-\":[\"카드\"],\"lCF0wC\":[\"새로고침\"],\"lHxVTh\":[\"Swimlane: Assignee\"],\"ltF1xa\":[\"병합 결과 저장\"],\"nabda1\":[\"카드 삭제\"],\"njJFtc\":[\"Delete comment\"],\"o7J4JM\":[\"필터\"],\"o8va6N\":[\"Restored\"],\"ojKCLU\":[\"담당자\"],\"p9yTeb\":[\"정렬: 제목\"],\"pKztsX\":[\"전체 에디터에서 열기\"],\"pnrmSP\":[\"새 카드\"],\"pwN6Ae\":[\"열 접기\"],\"pzutoc\":[\"기울임꼴\"],\"rdUucN\":[\"미리보기\"],\"sCzmvQ\":[\"개 카드\"],\"sQpDn6\":[\"전체 화면 종료\"],\"tK2x9T\":[\"⚠ 해결할 충돌 \",[\"0\"],\"건\",[\"1\"]],\"u2IprG\":[\"카드 제목 (Enter로 추가, Esc로 취소)\"],\"uAQUqI\":[\"상태\"],\"ucJg3u\":[\"Swimlane: Status\"],\"wf6Djn\":[\"기울임꼴 (Ctrl+I)\"],\"wtw-au\":[\"완료 열로 설정\"],\"wwu18a\":[\"아이콘\"],\"x52RAh\":[\"Blocked by \",[\"blockedCount\"],\" unfinished card(s)\"],\"y1eoq1\":[\"링크 복사\"],\"y9cj46\":[\"그룹: 우선순위\"],\"yEbJGs\":[\"+ Add field\"],\"ybGQtY\":[\"← 목록으로\"],\"yz7wBu\":[\"닫기\"],\"yzF66j\":[\"링크\"],\"zOc0vf\":[\"아이콘 없음\"],\"zga9sT\":[\"확인\"]}"); No newline at end of file
/*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"Blocked by\"],\"-b7T3G\":[\"Updated\"],\"1DBGsz\":[\"노트\"],\"1YABGm\":[\"링크 (Ctrl+K)\"],\"1hKEom\":[\"우선순위\"],\"1lWHP7\":[\"unsafe\"],\"2wxgft\":[\"이름 변경\"],\"3qkggm\":[\"전체 화면\"],\"4gdyen\":[\"로컈 (내 것)\"],\"4hJhzz\":[\"테이블\"],\"54sFiP\":[\"flowchart TD\\n A[시작] --> B[끝]\"],\"5Q_DQ6\":[\"인라인 코드\"],\"7VpPHA\":[\"확인\"],\"7s3WlU\":[\"Blocks\"],\"8PifYj\":[\"Mermaid 다이어그램\"],\"8hSn0h\":[\"결과 (편집 가능)\"],\"8lE269\":[\"정렬: 수동\"],\"9gxam6\":[\"이 Draw.io 다이어그램을 렌더링할 수 없습니다.\"],\"AC9Gkf\":[\"열 펼치기\"],\"AS5WO9\":[\"이 PDF를 렌더링할 수 없습니다.\"],\"AVreQ5\":[\"드래그하여 크기 조정\"],\"AgvHni\":[\"열 추가\"],\"AxAubu\":[\"그룹: 담당자\"],\"BfMZ7w\":[\"클라우드 수낙\"],\"BnmEvM\":[\"템플릿으로 저장\"],\"C6-ZRl\":[\"Someone\"],\"EWPtMO\":[\"코드\"],\"EbMPZJ\":[\"미할당\"],\"G4qrLy\":[\"완료 열 해제\"],\"GKu3m4\":[\"라벨 없음\"],\"Gpfctt\":[\"마감\"],\"H_SQFv\":[\"색상 없음\"],\"I6SWEy\":[\"스플릿\"],\"ICip_B\":[\"클라우드 (원격)\"],\"Ik60OC\":[\"에디터에서 열기\"],\"Iw6WJa\":[\"WIP 한도 설정\"],\"JTYvAw\":[\"카드 검색\"],\"K_F6pa\":[\"저장 중…\"],\"KjXDqG\":[\"Swimlane: None\"],\"KmydK6\":[\"굵게\"],\"KvW1VO\":[\"Draw.io 다이어그램\"],\"LQn6-8\":[\"로컈 수낙\"],\"MHrjPM\":[\"제목\"],\"Mm72la\":[\"No comments yet\"],\"NBdIgR\":[\"Comment\"],\"ONWvwQ\":[\"Upload\"],\"OYHzN1\":[\"태그\"],\"OepdfE\":[\"그룹: 상태\"],\"Pvpx7b\":[\"Paste a URL or path\"],\"Q2mGA7\":[\"필터 지우기\"],\"QD8opX\":[\"보드\"],\"QlsPZy\":[\"Mermaid 구문을 작성하면 다이어그램이 표시됩니다.\"],\"S5Qbb1\":[\"쉼표로 구분\"],\"TdfEV7\":[\"Archived\"],\"UQOvxZ\":[\"빈 카드\"],\"VNa_N2\":[\"이 파일 형식은 아직 미리볼 수 없습니다.\"],\"VbyRUy\":[\"Comments\"],\"WSP6v1\":[\"정렬: 우선순위\"],\"X03-eC\":[\"값을 입력해 주세요.\"],\"XJOV1Y\":[\"Activity\"],\"Ya7bZl\":[\"다이어그램 오류\"],\"Zot9XS\":[\"카드 없음\"],\"_5CsXX\":[\"완료 열\"],\"_EsjyQ\":[\"이것 사용\"],\"a6uhHr\":[\"굵게 (Ctrl+B)\"],\"aDvLhk\":[\"Add a comment…\"],\"abUZlY\":[\"세부정보 추가...\"],\"agOeRN\":[\"이 API 명세를 렌더링할 수 없습니다.\"],\"b4hVKD\":[\"색상 열\"],\"cfaWH-\":[\"라벨 추가\"],\"cnGeoo\":[\"삭제\"],\"d-F6q9\":[\"Created\"],\"d5z6xQ\":[\"WIP 한도 \",[\"0\"]],\"dEgA5A\":[\"취소\"],\"euc6Ns\":[\"복제\"],\"fYcKtB\":[\"정렬: 마감\"],\"gANddk\":[\"Uploading…\"],\"gLDJuJ\":[\"제목 없는 카드\"],\"hh4sEG\":[\"Relates\"],\"hnK1gR\":[\"PDF 문서\"],\"i4_LY_\":[\"작성\"],\"iTylMl\":[\"템플릿\"],\"iYVqZq\":[\"열 이름\"],\"jUbC3Z\":[\"Swimlane: Priority\"],\"jZlrte\":[\"색상\"],\"kZlRKE\":[\"Mermaid 소스\"],\"kryGs-\":[\"카드\"],\"lCF0wC\":[\"새로고침\"],\"lHxVTh\":[\"Swimlane: Assignee\"],\"ltF1xa\":[\"병합 결과 저장\"],\"nabda1\":[\"카드 삭제\"],\"njJFtc\":[\"Delete comment\"],\"o7J4JM\":[\"필터\"],\"o8va6N\":[\"Restored\"],\"ojKCLU\":[\"담당자\"],\"p9yTeb\":[\"정렬: 제목\"],\"pKztsX\":[\"전체 에디터에서 열기\"],\"pnrmSP\":[\"새 카드\"],\"pwN6Ae\":[\"열 접기\"],\"pzutoc\":[\"기울임꼴\"],\"rdUucN\":[\"미리보기\"],\"sCzmvQ\":[\"개 카드\"],\"sQpDn6\":[\"전체 화면 종료\"],\"tK2x9T\":[\"⚠ 해결할 충돌 \",[\"0\"],\"건\",[\"1\"]],\"t_YqKh\":[\"Remove\"],\"u2IprG\":[\"카드 제목 (Enter로 추가, Esc로 취소)\"],\"uAQUqI\":[\"상태\"],\"ucJg3u\":[\"Swimlane: Status\"],\"w7E-FA\":[\"Unsafe link blocked: \",[\"url\"]],\"w_Sphq\":[\"Attachments\"],\"wf6Djn\":[\"기울임꼴 (Ctrl+I)\"],\"wtw-au\":[\"완료 열로 설정\"],\"wwu18a\":[\"아이콘\"],\"x52RAh\":[\"Blocked by \",[\"blockedCount\"],\" unfinished card(s)\"],\"y1eoq1\":[\"링크 복사\"],\"y9cj46\":[\"그룹: 우선순위\"],\"yEbJGs\":[\"+ Add field\"],\"ybGQtY\":[\"← 목록으로\"],\"yz7wBu\":[\"닫기\"],\"yzF66j\":[\"링크\"],\"zOc0vf\":[\"아이콘 없음\"],\"zga9sT\":[\"확인\"]}"); No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Localize new attachment strings in the Korean catalog.

The new attachment-related entries are still English in this Korean message bundle (for example Upload, Paste a URL or path, Remove, Unsafe link blocked: {url}, Attachments), so this path won’t be fully localized.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/i18n/locales/ko/messages.mjs` at line 1, Translate the new
attachment-related message strings from English to Korean in the messages.mjs
file. Locate the following message keys in the JSON: ONWvwQ (Upload), Pvpx7b
(Paste a URL or path), t_YqKh (Remove), w7E-FA (Unsafe link blocked with url
parameter), and w_Sphq (Attachments), and replace their English values with
appropriate Korean translations to ensure the Korean locale is fully localized.

Comment thread shared/lib/board.ts
Comment on lines +72 to +83
/** Parse a frontmatter `attachments` value (comma-separated URLs/paths) into a list. */
export function parseAttachments(raw: string): string[] {
return raw
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}

/** Serialize attachment URLs/paths back to a frontmatter value. */
export function serializeAttachments(list: string[]): string {
return list.join(", ");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use a delimiter-safe format for attachment persistence.

At Line 73 and Line 82, comma is a structural delimiter. Valid vault paths/URLs can contain commas, so one attachment can be split into multiple entries on read/write, corrupting persisted card data. The same rule is mirrored in services/jtype-core/src/lib.rs (Line 1367), so this affects both platforms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/lib/board.ts` around lines 72 - 83, The parseAttachments and
serializeAttachments functions use commas as delimiters, but valid vault paths
and URLs can contain commas, causing data corruption when attachments with
commas are persisted and parsed back. Replace the comma delimiter with a safer
alternative (such as newline or pipe character) in both functions: update the
split(",") call in parseAttachments and the join(", ") call in
serializeAttachments to use the new delimiter consistently. Ensure the same
delimiter change is applied to the corresponding code in
services/jtype-core/src/lib.rs at Line 1367 to maintain consistency across both
platforms.

if (patch.due !== undefined) next.due = patch.due ?? "";
if (patch.icon !== undefined) next.icon = patch.icon ?? "";
if (patch.tags !== undefined) next.tags = patch.tags.map((tg) => tg.label).join(", ");
if (patch.attachments !== undefined) next.attachments = serializeAttachments(patch.attachments);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Delete attachments when clearing the list (Line 181).

Serializing [] to "" can round-trip as a bogus empty attachment with the comma-split verbatim scanner path. This should remove the frontmatter key instead of writing an empty string.

Suggested fix
-          if (patch.attachments !== undefined) next.attachments = serializeAttachments(patch.attachments);
+          if (patch.attachments !== undefined) {
+            if (patch.attachments.length > 0) next.attachments = serializeAttachments(patch.attachments);
+            else delete next.attachments;
+          }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/BoardView.tsx` at line 181, The line where patch.attachments
is processed needs to handle empty arrays specially. When patch.attachments is
an empty array, instead of calling serializeAttachments which converts it to an
empty string (causing round-trip issues), delete the attachments key from the
next object. Only call serializeAttachments and assign to next.attachments when
the attachments array has actual content. Modify the assignment at line 181 to
check if patch.attachments is non-empty before serializing, and delete
next.attachments when the array is empty.

@cnjack cnjack merged commit 1e78c6a into main Jun 23, 2026
3 checks passed
@cnjack cnjack deleted the feat/kanban-b3-attachments branch June 23, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant